2012-09-21 00:34:00
在上一篇《在 DLL 中加入第二个 COM 类》的“单用户注册”一节中,我们曾提到脱离注册表依赖一事,现在我们来把这事儿给办了。
我们在之前支持了“regsvr32 /n /i:user COMProvider.dll”这一注册命令。这一注册命令给了我们一定的扩展余地。从ATL默认的代码来看,对于DllInstall,目前已定义的命令行参数似乎只有user,于是我们可以定义自己的。
本文中,我们将从一个INI文件读入COM的相关信息,同时,也提供注册选项注册到INI文件。注册命令定义为:
因此,首先改造ComModule::DllInstall如下:
1STDMETHODIMP DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine)
2{
3 if (lpszCmdLine == nullptr)
4 {
5 return E_INVALIDARG;
6 }
7
8 String strCmdLine = lpszCmdLine;
9 String strCmdLineLower = strCmdLine.ToLower();
10
11 if (strCmdLineLower == _T("user"))
12 {
13 if (bInstall)
14 {
15 if (!RegisterTypeLib(HKEY_CURRENT_USER))
16 {
17 return E_FAIL;
18 }
19
20 if (!RegisterComClasses(HKEY_CURRENT_USER))
21 {
22 return E_FAIL;
23 }
24
25 return S_OK;
26 }
27 else
28 {
29 if (!UnregisterComClasses(HKEY_CURRENT_USER))
30 {
31 return E_FAIL;
32 }
33
34 if (!UnregisterTypeLib(HKEY_CURRENT_USER))
35 {
36 return E_FAIL;
37 }
38
39 return S_OK;
40 }
41 }
42
43 if (strCmdLineLower == _T("ini") || strCmdLineLower.IndexOf(_T("ini:")) == 0)
44 {
45 LPCTSTR DEFAULT_INI_FILENAME = _T("xlComReg.ini");
46 String strIniFileName = DEFAULT_INI_FILENAME;
47
48 if (strCmdLine.Length() > 4)
49 {
50 strIniFileName = strCmdLine.SubString(4);
51
52 if (strIniFileName[strIniFileName.Length() - 1] == _T('\\'))
53 {
54 strIniFileName += DEFAULT_INI_FILENAME;
55 }
56 }
57
58 if (bInstall)
59 {
60 if (!RegisterTypeLibToIni(strIniFileName))
61 {
62 return E_FAIL;
63 }
64
65 if (!RegisterComClassesToIni(strIniFileName))
66 {
67 return E_FAIL;
68 }
69
70 return S_OK;
71 }
72 else
73 {
74 if (!UnregisterComClassesFromIni(strIniFileName))
75 {
76 return E_FAIL;
77 }
78
79 if (!UnregisterTypeLibFromIni(strIniFileName))
80 {
81 return E_FAIL;
82 }
83
84 return S_OK;
85 }
86 }
87
88 return E_FAIL;
89}
默认INI名字定为xlComReg.ini。这里调用了四个函数:
1bool RegisterTypeLibToIni(const String &strIniFileName)
2{
3 if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("TypeLib"), m_strLibName))
4 {
5 return false;
6 }
7
8 if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Version"), m_strLibVersion))
9 {
10 return false;
11 }
12
13 String strModulePath = GetModuleRelativePathToIni(strIniFileName);
14
15#ifdef _WIN64
16 if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Win64"), strModulePath))
17 {
18 return false;
19 }
20#else
21 if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Win32"), strModulePath))
22 {
23 return false;
24 }
25#endif
26 return true;
27}
1bool UnregisterTypeLibFromIni(const String &strIniFileName)
2{
3 if (!IniFile::DeleteSection(strIniFileName, m_strLibID))
4 {
5 return false;
6 }
7
8 return true;
9}
1bool RegisterComClassesToIni(const String &strIniFileName)
2{
3 for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
4 {
5 if (*ppEntry == nullptr)
6 {
7 continue;
8 }
9
10 TCHAR szClassID[40] = {};
11 StringFromGUID2(*(*ppEntry)->pClsid, szClassID, ARRAYSIZE(szClassID));
12
13 String strVersionIndependentProgID = (*ppEntry)->lpszProgID;
14 String strProgID = strVersionIndependentProgID + _T(".") + (*ppEntry)->lpszVersion;
15
16 if (!IniFile::SetValue(strIniFileName, szClassID, _T("Class"), (*ppEntry)->lpszClassDesc))
17 {
18 return false;
19 }
20
21 String strModulePath = GetModuleRelativePathToIni(strIniFileName);
22
23#ifdef _WIN64
24 if (!IniFile::SetValue(strIniFileName, szClassID, _T("InprocServer64"), strModulePath))
25 {
26 return false;
27 }
28#else
29 if (!IniFile::SetValue(strIniFileName, szClassID, _T("InprocServer32"), strModulePath))
30 {
31 return false;
32 }
33#endif
34
35 if (!m_strLibID.Empty())
36 {
37 if (!IniFile::SetValue(strIniFileName, szClassID, _T("TypeLib"), m_strLibID))
38 {
39 return false;
40 }
41 }
42
43 if (!strProgID.Empty())
44 {
45 if (!IniFile::SetValue(strIniFileName, szClassID, _T("ProgID"), strProgID))
46 {
47 return false;
48 }
49
50 if (!IniFile::SetValue(strIniFileName, strProgID, _T("Class"), (*ppEntry)->lpszClassDesc))
51 {
52 return false;
53 }
54
55 if (!IniFile::SetValue(strIniFileName, strProgID, _T("CLSID"), szClassID))
56 {
57 return false;
58 }
59 }
60
61 if (!strVersionIndependentProgID.Empty())
62 {
63 if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("Class"), (*ppEntry)->lpszClassDesc))
64 {
65 return false;
66 }
67
68 if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("CurVer"), strProgID))
69 {
70 return false;
71 }
72
73 if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("CLSID"), szClassID))
74 {
75 return false;
76 }
77 }
78 }
79
80 return true;
81}
1bool UnregisterComClassesFromIni(const String &strIniFileName)
2{
3 for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
4 {
5 if (*ppEntry == nullptr)
6 {
7 continue;
8 }
9
10 TCHAR szClassID[40] = {};
11 StringFromGUID2(*(*ppEntry)->pClsid, szClassID, ARRAYSIZE(szClassID));
12
13 String strVersionIndependentProgID = (*ppEntry)->lpszProgID;
14 String strProgID = strVersionIndependentProgID + _T(".") + (*ppEntry)->lpszVersion;
15
16 if (!IniFile::DeleteSection(strIniFileName, szClassID))
17 {
18 return false;
19 }
20
21 if (!strProgID.Empty())
22 {
23 if (!IniFile::DeleteSection(strIniFileName, strProgID))
24 {
25 return false;
26 }
27 }
28
29 if (!strVersionIndependentProgID.Empty())
30 {
31 if (!IniFile::DeleteSection(strIniFileName, strVersionIndependentProgID))
32 {
33 return false;
34 }
35 }
36 }
37
38 return true;
39}
其中DLL路径用的是DLL相对于INI的相对路径,用函数GetModuleRelativePathToIni获取,该函数的实现如下:
1String GetModuleRelativePathToIni(const String &strIniFileName)
2{
3 TCHAR szIniPathAbsolute[MAX_PATH] = {};
4
5 if (GetFullPathName(strIniFileName.GetAddress(), ARRAYSIZE(szIniPathAbsolute), szIniPathAbsolute, nullptr) == 0)
6 {
7 return m_strModulePath;
8 }
9
10 TCHAR szModuleRelativePath[MAX_PATH] = {};
11
12 if (!PathRelativePathTo(szModuleRelativePath, szIniPathAbsolute, 0, m_strModulePath.GetAddress(), 0))
13 {
14 return m_strModulePath;
15 }
16
17 return szModuleRelativePath;
18}
上面代码将一个COM在注册表中的所有信息全部写到了INI。其实这是不必要的,对于C++程序来说,要使用这个COM,可只需要知道CLSID对应到哪个DLL就可以了。因此,上面划线的代码可以去掉不用,不影响后续使用。
好了,运行“regsvr32 /n /i:ini COMProvider.dll”,生成xlComReg.ini,内容如下:
1[{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}]
2Class=Streamlet COMProvider Sample Class
3InprocServer32=.\COMProvider.dll
注册好了,该使用了。这就涉及COM的加载过程了。简单的说,我们一般先CoInitialize,然后CoCreateInstance拿到对象去使用,完了之后CoUninitialize使用完毕。现在我们就来模拟这个过程。除了这三个函数以外,我们还将模拟CoGetClassObject以及CoFreeUnusedLibraries。
于是加载器接口定义为:
1struct __declspec(uuid("FE52639A-5B41-49B0-9A50-7A1C4FBC83E2"))
2IComLoader : public IDispatch
3{
4 virtual HRESULT CoInitialize(_In_opt_ LPVOID pvReserved) PURE;
5 virtual void CoUninitialize() PURE;
6 virtual void CoFreeUnusedLibraries() PURE;
7 virtual HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) PURE;
8 virtual HRESULT CoCreateInstance(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) PURE;
9};
ComLoader本身将作为一个Com类实现,因此我在IComLoader的声明中加上了UUID。然后我们针对注册到INI的COM写一个Loader。
相关数据结构定义如下:
1typedef HRESULT (__stdcall *FnDllCanUnloadNow)();
2typedef HRESULT (__stdcall *FnDllGetClassObject)(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv);
3
4struct ComDllModule
5{
6 String strFileName;
7 HMODULE hModule;
8 FnDllCanUnloadNow fnDllCanUnloadNow;
9 FnDllGetClassObject fnDllGetClassObject;
10
11 ComDllModule() : hModule(nullptr), fnDllCanUnloadNow(nullptr), fnDllGetClassObject(nullptr)
12 {
13
14 }
15};
16
17typedef Map<String, String> ClassIDPathMap;
18typedef Map<String, ComDllModule> PathModuleMap;
前面两行定义函数指针,这两个函数是COM DLL导出的。对于每个被加载的DLL,我们将查找这两个函数入口。ComDllModule结构用于保存一个已加载的COM DLL的信息。各分量意义很明白了,不解释。最后两个Map,一个是用于存储从INI读入的CLSID到DLL路径的对应关系,另一个是存储DLL加载后,DLL路径到ComDllModule结构的对应关系。
下面是ComLoaderFromIni的框架性定义:
1class ComLoaderFromIni : public ComClass<ComLoaderFromIni>,
2 public Dispatcher<IComLoader>
3{
4public:
5 ComLoaderFromIni(const String &strIniFile =_T("xlComReg.ini")) :
6 m_strIniFile(strIniFile), m_lInitializeCount(0)
7 {
8
9 }
10
11 ~ComLoaderFromIni()
12 {
13 CoUninitialize();
14 }
15
16public:
17 HRESULT CoInitialize(_In_opt_ LPVOID pvReserved)
18 {
19 return S_OK;
20 }
21
22 void CoUninitialize()
23 {
24
25 }
26
27 void CoFreeUnusedLibraries()
28 {
29
30 }
31
32 HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv)
33 {
34 return S_OK;
35 }
36
37private:
38 HRESULT FindComDllModule(REFCLSID rclsid, const ComDllModule **ppModule)
39 {
40 return S_OK;
41 }
42
43 bool LoadComDll(const String &strFileName)
44 {
45 return true;
46 }
47
48private:
49 String m_strIniFile;
50 LONG m_lInitializeCount;
51 ClassIDPathMap m_mapClassIDToPath;
52 PathModuleMap m_mapPathToModule;
53 CriticalSection m_cs;
54
55public:
56 XL_COM_INTERFACE_BEGIN(ComLoaderFromIni)
57 XL_COM_INTERFACE(IComLoader)
58 XL_COM_INTERFACE(IDispatch)
59 XL_COM_INTERFACE_END()
60};
其中主要函数目前还没实现。成员变量中有个m_lInitializeCount,是给CoInitialize和CoUninitialize做引用计数的,CriticalSection是给两个Map加锁用的。其余变量的意义很明白,也不介绍了。
首先看最后一个函数,LoadComDll。它用于加载指定的COM DLL,并将模块信息存入Map。实现如下:
1bool LoadComDll(const String &strFileName)
2{
3 XL_SCOPED_CRITICAL_SECTION(m_cs);
4
5 HMODULE hModule = LoadLibrary(strFileName.GetAddress());
6
7 if (hModule == nullptr)
8 {
9 return false;
10 }
11
12 ScopeGuard sgFreeLibrary = MakeGuard(Bind(FreeLibrary, hModule));
13
14 FnDllCanUnloadNow fnDllCanUnloadNow = (FnDllCanUnloadNow)GetProcAddress(hModule, "DllCanUnloadNow");
15
16 if (fnDllCanUnloadNow == nullptr)
17 {
18 return false;
19 }
20
21 FnDllGetClassObject fnDllGetClassObject = (FnDllGetClassObject)GetProcAddress(hModule, "DllGetClassObject");
22
23 if (fnDllGetClassObject == nullptr)
24 {
25 return false;
26 }
27
28 ComDllModule &module = m_mapPathToModule[strFileName];
29 module.strFileName = strFileName;
30 module.hModule = hModule;
31 module.fnDllCanUnloadNow = fnDllCanUnloadNow;
32 module.fnDllGetClassObject = fnDllGetClassObject;
33
34 sgFreeLibrary.Dismiss();
35
36 return true;
37}
倒数第二个函数,FindComDllModule,定义为从CLSID找到ComDllModule。首先从m_mapClassIDToPath找到路径,再尝试从m_mapPathToModule找到ComModule。如果未找到,那就尝试使用上面的LoadComDll加载它。代码如下:
1HRESULT FindComDllModule(REFCLSID rclsid, const ComDllModule **ppModule)
2{
3 XL_SCOPED_CRITICAL_SECTION(m_cs);
4
5 if (ppModule == nullptr)
6 {
7 return E_INVALIDARG;
8 }
9
10 *ppModule = nullptr;
11
12 TCHAR szClassID[40] = {};
13 StringFromGUID2(rclsid, szClassID, ARRAYSIZE(szClassID));
14
15 auto itPath = m_mapClassIDToPath.Find(szClassID);
16
17 if (itPath == m_mapClassIDToPath.End())
18 {
19 return REGDB_E_CLASSNOTREG;
20 }
21
22 auto itModule = m_mapPathToModule.Find(itPath->Value);
23
24 if (itModule == m_mapPathToModule.End())
25 {
26 if (!LoadComDll(itPath->Value))
27 {
28 return E_FAIL;
29 }
30
31 itModule = m_mapPathToModule.Find(itPath->Value);
32 }
33
34 if (itModule == m_mapPathToModule.End())
35 {
36 return E_FAIL;
37 }
38
39 *ppModule = &itModule->Value;
40
41 return S_OK;
42}
下面按使用流程分别介绍五个标准函数。首先是CoInitialize,它主要就是从INI读取CLSID到DLL路径的对应关系,代码如下:
1HRESULT CoInitialize(_In_opt_ LPVOID pvReserved)
2{
3 XL_SCOPED_CRITICAL_SECTION(m_cs);
4
5 if (m_lInitializeCount > 0)
6 {
7 InterlockedIncrement(&m_lInitializeCount);
8 return S_FALSE;
9 }
10
11 Array<String> arrSections;
12
13 if (!IniFile::EnumSections(m_strIniFile, &arrSections))
14 {
15 return E_FAIL;
16 }
17
18 for (auto it = arrSections.Begin(); it != arrSections.End(); ++it)
19 {
20 String strClass;
21
22 if (!IniFile::GetValue(m_strIniFile, *it, _T("Class"), &strClass))
23 {
24 continue;
25 }
26
27 String strPath;
28
29#ifdef _WIN64
30 if (!IniFile::GetValue(m_strIniFile, *it, _T("InprocServer64"), &strPath))
31 {
32 continue;
33 }
34#else
35 if (!IniFile::GetValue(m_strIniFile, *it, _T("InprocServer32"), &strPath))
36 {
37 continue;
38 }
39#endif
40
41 m_mapClassIDToPath.Insert(*it, strPath);
42 }
43
44 InterlockedIncrement(&m_lInitializeCount);
45
46 return S_OK;
47}
需要留意的就是引用计数处理,这使得多次调用CoInitialze也是安全的。
这个函数用于取得类厂。由于上面已经准备了FindComDllModule,直接调用获取到ComDllModule信息,然后调用COM DLL导出的DllGetClassObject就可以了:
1HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv)
2{
3 XL_SCOPED_CRITICAL_SECTION(m_cs);
4
5 const ComDllModule *pModule = nullptr;
6 HRESULT hr = FindComDllModule(rclsid, &pModule);
7
8 if (FAILED(hr))
9 {
10 return hr;
11 }
12
13 return pModule->fnDllGetClassObject(rclsid, riid, ppv);
14}
由于上面已经可以拿到类厂了,这里直接调用,获取类厂后调用类厂的CreateInstance创建对象:
1HRESULT CoCreateInstance(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv)
2{
3 IClassFactory *pClassFactory = nullptr;
4 HRESULT hr = CoGetClassObject(rclsid, __uuidof(IClassFactory), (LPVOID *)&pClassFactory);
5
6 if (FAILED(hr))
7 {
8 return hr;
9 }
10
11 hr = pClassFactory->CreateInstance(NULL, riid, ppv);
12 pClassFactory->Release();
13
14 return hr;
15}
这个函数用于清理不再使用的COM DLL。实现思路就是遍历已加载的模块信息,调用DllCanUnloadNow,如果DLL返回S_OK,将其卸载:
1void CoFreeUnusedLibraries()
2{
3 XL_SCOPED_CRITICAL_SECTION(m_cs);
4
5 for (auto it = m_mapPathToModule.Begin(); it != m_mapPathToModule.End(); )
6 {
7 if (it->Value.fnDllCanUnloadNow() == S_OK)
8 {
9 FreeLibrary(it->Value.hModule);
10 it = m_mapPathToModule.Delete(it);
11 }
12 else
13 {
14 ++it;
15 }
16 }
17}
这是最终的卸载函数,卸载所有已加载的DLL,清除所有信息:
1void CoUninitialize()
2{
3 XL_SCOPED_CRITICAL_SECTION(m_cs);
4
5 if (m_lInitializeCount == 0)
6 {
7 return;
8 }
9
10 InterlockedDecrement(&m_lInitializeCount);;
11
12 if (m_lInitializeCount > 0)
13 {
14 return;
15 }
16
17 for (auto it = m_mapPathToModule.Begin(); it != m_mapPathToModule.End(); ++it)
18 {
19 FreeLibrary(it->Value.hModule);
20 }
21
22 m_mapClassIDToPath.Clear();
23 m_mapPathToModule.Clear();
24}
需要注意的是,这里也有引用计数的处理,与CoInitialize重的对应。
使用之前,先为刚才的加载器创建一个工厂函数吧(当然,直接使用也没关系):
1enum ComLoadType
2{
3 CLT_FROM_INI,
4};
5
6inline IComLoader *CreateComLoader(ComLoadType type, const String &strData = _T("xlComReg.ini"))
7{
8 IComLoader *pLoader = nullptr;
9
10 switch (type)
11 {
12 case CLT_FROM_INI:
13 pLoader = new ComLoaderFromIni(strData);
14 pLoader->AddRef();
15 break;
16 default:
17 break;
18 }
19
20 return pLoader;
21}
目前只有INI加载器。
使用方式如下:
1int _tmain(int argc, TCHAR *argv[])
2{
3 xl::IComLoader *pComLoader = xl::CreateComLoader(xl::CLT_FROM_INI);
4 HRESULT hr = pComLoader->CoInitialize(NULL);
5
6 ISampleInterface *pSampleInterface = nullptr;
7 hr = pComLoader->CoCreateInstance(__uuidof(SampleClass),
8 __uuidof(ISampleInterface),
9 (LPVOID *)&pSampleInterface);
10
11 if (SUCCEEDED(hr))
12 {
13 pSampleInterface->SampleMethod();
14 pSampleInterface->Release();
15 }
16
17 pComLoader->Release();
18
19 return 0;
20}
运行结果:
以上,框架代码见: http://xllib.codeplex.com/SourceControl/changeset/view/20034#319450 例子代码见COMProtocol4.rar(http://pan.baidu.com/s/1qWJSeS0)
在本文中,我们脱离了注册表依赖,将COM信息存到了本目录文件,这意味着我们在发布的时候可以事先生成这个文件,或者可以由安装程序生成。至此,我们给出了使用COM DLL作为替代普通DLL的一整套方案。如果能忍受COM接口讨厌的有限的变量类型,以及冗长的行文方式,COM DLL的形式将比普通DLL导出函数更具有优势,可以作为动态链接库实现形式的一般解决方案。
等等,有人可能会问:套间模型哪去了呢?
——套间模型是什么?这显然不影响我们将COM DLL用于替代普通DLL的整个过程。不需要这个概念。
首发:http://www.cppblog.com/Streamlet/archive/2012/09/21/191436.html